SQC-871 Implements socket serialization for transferring socket over RPC#6863
SQC-871 Implements socket serialization for transferring socket over RPC#6863Ltadrian wants to merge 2 commits into
Conversation
Ltadrian
commented
Jul 6, 2026
- Adds TransferredSocketStream class that handles implementing underlying AsyncIoStream IO operations for deserializing socket
- Socket deserialization unwraps raw kj streams and builds non-deferred stream to pass into TransferredSocketStream
- Adds js-rpc-socket-test tests
- Add outbound interceptor test to showcase full end to end use case of loopback binding
RPC - Adds TransferredSocketStream class that handles implementing underlying AsyncIoStream IO operations - Socket serialization/deserialzation unwraps raw kj streams and builds non-deferred sockets - Fix and assert existing test that returns socket over RPC and can read bytes - Add outbound interceptor test to showcase returning socket over RPC
| // Serialize the readable and writable streams as separate externals | ||
| readable->serialize(js, serializer); | ||
|
|
||
| writable->serialize(js, serializer); |
There was a problem hiding this comment.
Be aware the the JsReadableStream and JsWritableStream work necessarily will impact this. JsReadableStream has already landed. readable is no longer a jsg::Ref<ReadableStream> but is wrapped by a JsReadableStream. The serialization story is still a work in progress.
There was a problem hiding this comment.
Thank you I wasn't aware of JsReadableStream before working on this. I've caught up on the implementation in the internal repo workerd and it looks like it hopefully be too much of a lift to refactor some of these parts. I'll keep my eye out for when this lands in public repo 😊 Do you think it would be a blocker to support both JsReadableStream/JsWritableStream before landing this socket serialization?
| // JsRpcCustomEvent is canceled), so their pumpTo() must not be deferred past the context's | ||
| // lifetime. | ||
| sysStreams.readable = newNoDeferredProxyReadableStream(kj::mv(sysStreams.readable), ioContext); | ||
| auto readable = js.alloc<ReadableStream>(ioContext, kj::mv(sysStreams.readable)); |
There was a problem hiding this comment.
With JsReadableStream already landed, this would become JsReadableStream::create(...)
There was a problem hiding this comment.
(JsReadableStream is not yet in the public repo... should arrive there soon)
| // No opened-gate on the writable: the connection is already established (opened resolves below), | ||
| // and passing a gate is unnecessary here. | ||
| auto writable = js.alloc<WritableStream>(ioContext, kj::mv(sysStreams.writable), | ||
| ioContext.getMetrics().tryCreateWritableByteStreamObserver()); |
There was a problem hiding this comment.
this would change once JsWritableStream lands
|
A few questions...
|
dom96
left a comment
There was a problem hiding this comment.
Yeah, like James mentioned, I'd repeat that currently as things stand there is a need for more synchronisation with the various socket states (closed/open). But perhaps that's planned since this is still a draft :)
In general though I think this is a good start. I left a few comments about various things I'd improve as well, but nothing major.
|
Thank you for the initial review, I have spent some extra time addressing the feedback and have added a second commit to this merge request to handle the various states I initially put on TODO: I'll re-review my second commit today again but this seems to be functional now |
(opened/closed/allowHalfOpen/secureTransport) Carry the origin socket's runtime state over JS RPC and reconstruct equivalent behavior on the receiving side. - Wire format: add secureTransport (new enum), isDefaultFetchPort, localAddress, and allowHalfOpen to External.socket; fix the socket external doc comment. - opened: track settled state (OpenedState enum); serialize() throws DataCloneError unless OPENED; deserialize() resolves it from the transferred SocketInfo and marks the socket OPENED. - allowHalfOpen: carried across transfer; when false, re-establish the read-EOF -> write-side auto-close on the receiving side. - closed: extract shared disconnect wiring (used by setupSocket) and resolve `closed` on disconnect for transferred sockets. - deserialize scope: the EOF and closed wiring attach jsg `.then()` continuations (illegal under the no-JS deserialize scope), so defer them to microtasks; kj-side signals are latched synchronously so nothing is missed. - Tests: allowHalfOpen honored across transfer, unopened transfer throws, `closed` resolves on close(); js-rpc-test awaits `opened` before returning.
b7082f5 to
a44dee4
Compare
Merging this PR will improve performance by 9.77%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | bodyWithHeaders[Response] |
34.2 µs | 31.2 µs | +9.77% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing Ltadrian:dlapid/jsrpc_socket (a44dee4) with main (a77b7aa)
Footnotes
-
129 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩